from __future__ import annotations

import asyncio
import datetime as dt
import json
import os
import uuid
from collections import defaultdict, deque
from pathlib import Path
from threading import Lock
from typing import Iterable, Literal

from fastapi import Depends, FastAPI, HTTPException, Query
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import StreamingResponse
from pydantic import BaseModel, Field
from sqlalchemy import Column, DateTime, ForeignKey, Integer, String, create_engine, func
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session, declarative_base, relationship, sessionmaker

from agent_society.memory.event_log import read_events
from agent_society.memory.records import events_to_records

DATABASE_URL = "sqlite:///./compute_ledger.db"

engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
Base = declarative_base()


class Account(Base):
    __tablename__ = "accounts"

    id: int = Column(Integer, primary_key=True, index=True)
    name: str = Column(String, unique=True, index=True)
    balance: int = Column(Integer, default=0)
    created_at: dt.datetime = Column(DateTime, default=func.now())

    entries = relationship("LedgerEntry", back_populates="account")


class LedgerEntry(Base):
    __tablename__ = "ledger_entries"

    id: int = Column(Integer, primary_key=True, index=True)
    account_id: int = Column(Integer, ForeignKey("accounts.id"))
    delta: int = Column(Integer)
    reason: str = Column(String)
    correlation_id: str = Column(String, nullable=True)
    created_at: dt.datetime = Column(DateTime, default=func.now())

    account = relationship("Account", back_populates="entries")


Base.metadata.create_all(bind=engine)


AGENT_CONFIG_PATH = Path(os.environ.get("AGENT_CONFIG_PATH", "config/agents.json"))
EVENT_LOG_DIR = Path(os.environ.get("AGENT_EVENT_DIR", "events"))
TASK_STORE_PATH = Path(os.environ.get("GOVERNANCE_TASK_STORE", "data/governance_tasks.json"))
PRESENCE_STORE_PATH = Path(os.environ.get("AGENT_PRESENCE_STORE", "data/agent_presence.json"))
TIMELINE_PATH = Path(os.environ.get("AGENT_TIMELINE_PATH", "data/codex_timeline.jsonl"))
PERSONA_SUMMARY_PATH = Path(os.environ.get("AGENT_PERSONA_SUMMARIES", "data/persona_summaries.json"))

_TASK_LOCK = Lock()


class AccountCreate(BaseModel):
    name: str = Field(..., description="Unique account identifier")
    initial_balance: int = Field(0, ge=0)


class AccountView(BaseModel):
    name: str
    balance: int

    class Config:
        from_attributes = True


class CreditOperation(BaseModel):
    amount: int = Field(..., gt=0)
    reason: str = Field(..., min_length=1)
    correlation_id: str | None = None


class TransferOperation(BaseModel):
    destination: str = Field(..., description="Destination account name")
    amount: int = Field(..., gt=0)
    reason: str
    correlation_id: str | None = None


class LedgerEntryView(BaseModel):
    delta: int
    reason: str
    correlation_id: str | None
    created_at: dt.datetime

    class Config:
        from_attributes = True


class AgentSummary(BaseModel):
    id: str
    name: str
    role: str
    status: str
    balance: int
    objectives: list[str] = Field(default_factory=list)
    traits: dict[str, str] = Field(default_factory=dict)
    presence: AgentPresence


class LedgerSummary(BaseModel):
    totalCredits: int
    velocity: float
    issuance: int
    charges: int
    updatedAt: dt.datetime


class EventRecord(BaseModel):
    id: str
    type: str
    owner: str
    actor: str
    createdAt: dt.datetime
    content: str
    headers: dict[str, str]


class GovernanceTaskPayload(BaseModel):
    owner: str
    actor: str
    title: str
    description: str | None = None
    assignees: list[str] | None = None


class GovernanceTask(BaseModel):
    id: str
    owner: str
    actor: str
    title: str
    status: str
    description: str | None = None
    assignees: list[str] = Field(default_factory=list)
    createdAt: dt.datetime


class StatusUpdate(BaseModel):
    status: str


class TimelineEntry(BaseModel):
    timestamp: dt.datetime
    ended_at: dt.datetime | None = None
    agent: str
    role: str | None = None
    client: str | None = None
    type: str = "agent"
    prompt: str | None = None
    returncode: int | None = None
    duration: float | None = None
    output: str | None = None
    stderr: str | None = None


class AgentPresence(BaseModel):
    state: Literal["online", "away", "offline"]
    sessionId: str | None = None
    client: str | None = None
    capabilities: list[str] = Field(default_factory=list)
    connectedAt: dt.datetime | None = None
    lastSeen: dt.datetime | None = None
    disconnectedAt: dt.datetime | None = None


class AgentPresenceView(BaseModel):
    agent_id: str = Field(..., alias="agentId")
    presence: AgentPresence

    class Config:
        allow_population_by_field_name = True


class SessionCreate(BaseModel):
    agent_id: str = Field(..., alias="agentId")
    client: str = Field(..., min_length=1)
    capabilities: list[str] = Field(default_factory=list)

    class Config:
        allow_population_by_field_name = True


class SessionHeartbeat(BaseModel):
    state: Literal["online", "away"] = "online"
    capabilities: list[str] | None = None


class SessionView(BaseModel):
    session_id: str = Field(..., alias="sessionId")
    agent_id: str = Field(..., alias="agentId")
    presence: AgentPresence

    class Config:
        allow_population_by_field_name = True


class MemoryRecordView(BaseModel):
    id: str
    owner: str
    actor: str
    content: str
    importance: float
    recency: float
    relevance: float
    score: float
    createdAt: dt.datetime
    lastAccessed: dt.datetime


class EnvironmentNode(BaseModel):
    id: str
    label: str
    description: str | None = None
    tags: dict[str, str] = Field(default_factory=dict)
    children: list["EnvironmentNode"] = Field(default_factory=list)


class EnvironmentView(BaseModel):
    tree: EnvironmentNode
    context: str


SessionCreate.model_rebuild()
SessionView.model_rebuild()
EnvironmentNode.model_rebuild()


app = FastAPI(title="Compute Credit Ledger", version="0.1")
app.add_middleware(
    CORSMiddleware,
    allow_origins=["*"],
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
)


def get_db():
    db = SessionLocal()
    try:
        yield db
    finally:
        db.close()


def _get_account(db: Session, name: str) -> Account:
    account = db.query(Account).filter(Account.name == name).first()
    if not account:
        raise HTTPException(status_code=404, detail=f"Account '{name}' not found")
    return account


def _record_entry(db: Session, account: Account, delta: int, reason: str, correlation_id: str | None) -> None:
    account.balance += delta
    entry = LedgerEntry(account=account, delta=delta, reason=reason, correlation_id=correlation_id)
    db.add(entry)


def _load_agent_config() -> dict[str, dict[str, object]]:
    if AGENT_CONFIG_PATH.exists():
        try:
            data = json.loads(AGENT_CONFIG_PATH.read_text(encoding="utf-8"))
            return {entry["id"]: entry for entry in data if "id" in entry}
        except Exception:
            return {}
    return {}


def _load_tasks() -> list[dict[str, object]]:
    if TASK_STORE_PATH.exists():
        try:
            return json.loads(TASK_STORE_PATH.read_text(encoding="utf-8"))
        except Exception:
            return []
    return []


def _save_tasks(tasks: list[dict[str, object]]) -> None:
    TASK_STORE_PATH.parent.mkdir(parents=True, exist_ok=True)
    TASK_STORE_PATH.write_text(json.dumps(tasks, ensure_ascii=False, indent=2), encoding="utf-8")


def _load_presence() -> dict[str, dict[str, object]]:
    if PRESENCE_STORE_PATH.exists():
        try:
            data = json.loads(PRESENCE_STORE_PATH.read_text(encoding="utf-8"))
            if isinstance(data, dict):
                return {str(k): dict(v) for k, v in data.items() if isinstance(v, dict)}
        except Exception:
            return {}
    return {}


def _save_presence(presence: dict[str, dict[str, object]]) -> None:
    PRESENCE_STORE_PATH.parent.mkdir(parents=True, exist_ok=True)
    PRESENCE_STORE_PATH.write_text(json.dumps(presence, ensure_ascii=False, indent=2), encoding="utf-8")


def _load_persona_summaries() -> dict[str, dict[str, object]]:
    if not PERSONA_SUMMARY_PATH.exists():
        return {}
    try:
        data = json.loads(PERSONA_SUMMARY_PATH.read_text(encoding="utf-8"))
    except Exception:
        return {}
    if isinstance(data, dict):
        return {str(k): dict(v) for k, v in data.items() if isinstance(v, dict)}
    return {}


def _parse_iso(value: str | None) -> dt.datetime | None:
    if not value:
        return None
    normalized = value.replace("Z", "+00:00")
    try:
        parsed = dt.datetime.fromisoformat(normalized)
    except ValueError:
        return None
    if parsed.tzinfo is None:
        parsed = parsed.replace(tzinfo=dt.timezone.utc)
    return parsed


def _iso_now() -> str:
    return dt.datetime.utcnow().replace(tzinfo=dt.timezone.utc).isoformat()


def _ensure_presence(agent_id: str, presence_store: dict[str, dict[str, object]]) -> dict[str, object]:
    record = presence_store.get(agent_id)
    if not record:
        record = {
            "state": "offline",
            "sessionId": None,
            "client": None,
            "capabilities": [],
            "connectedAt": None,
            "lastSeen": None,
            "disconnectedAt": _iso_now(),
        }
        presence_store[agent_id] = record
    return record


def _presence_view(agent_id: str, record: dict[str, object]) -> AgentPresence:
    state = str(record.get("state", "offline"))
    if state not in {"online", "offline", "away"}:
        state = "offline"
    return AgentPresence(
        state=state, 
        sessionId=record.get("sessionId"),
        client=record.get("client"),
        capabilities=list(record.get("capabilities", [])),
        connectedAt=_parse_iso(record.get("connectedAt")),
        lastSeen=_parse_iso(record.get("lastSeen")),
        disconnectedAt=_parse_iso(record.get("disconnectedAt")),
    )


def _read_timeline_raw() -> list[dict[str, object]]:
    if not TIMELINE_PATH.exists():
        return []
    entries: list[dict[str, object]] = []
    try:
        with TIMELINE_PATH.open("r", encoding="utf-8") as fh:
            for line in fh:
                text = line.strip()
                if not text:
                    continue
                try:
                    entries.append(json.loads(text))
                except json.JSONDecodeError:
                    continue
    except OSError:
        return []
    return entries


def _parse_timeline_entry(payload: dict[str, object]) -> TimelineEntry:
    ts = _parse_iso(payload.get("timestamp")) or dt.datetime.utcnow().replace(tzinfo=dt.timezone.utc)
    ended = _parse_iso(payload.get("ended_at"))
    return TimelineEntry(
        timestamp=ts,
        ended_at=ended,
        agent=str(payload.get("agent", "")),
        role=payload.get("role"),
        client=payload.get("client"),
        type=str(payload.get("type", "agent")),
        prompt=payload.get("prompt"),
        returncode=payload.get("returncode"),
        duration=payload.get("duration"),
        output=payload.get("output"),
        stderr=payload.get("stderr"),
    )


def _timeline_entries(limit: int) -> list[TimelineEntry]:
    raw_entries = _read_timeline_raw()
    if limit > 0:
        raw_entries = raw_entries[-limit:]
    return [_parse_timeline_entry(entry) for entry in raw_entries]


def _find_agent_by_session(session_id: str, presence_store: dict[str, dict[str, object]]) -> tuple[str, dict[str, object]]:
    for agent_id, record in presence_store.items():
        if record.get("sessionId") == session_id:
            return agent_id, record
    raise HTTPException(status_code=404, detail="Session not found")


def _agent_environment_node(
    agent_id: str,
    profile: dict[str, object],
    presence_store: dict[str, dict[str, object]],
    summaries: dict[str, dict[str, object]],
    tasks: list[dict[str, object]],
    account: Account | None,
    *,
    max_tasks: int,
) -> EnvironmentNode:
    presence_record = presence_store.get(agent_id, {})
    presence = _presence_view(agent_id, dict(presence_record)) if presence_record else AgentPresence(state="offline")
    summary = summaries.get(agent_id, {})
    role = str(profile.get("role", "agent"))
    objectives = ", ".join(profile.get("objectives", [])) if profile.get("objectives") else "(none)"
    traits = profile.get("traits", {})
    traits_line = ", ".join(f"{k}: {v}" for k, v in traits.items()) if traits else "(none)"
    balance = account.balance if account else 0
    description_parts = [f"Role: {role}", f"Credits: {balance} cr", f"Presence: {presence.state}"]
    if presence.client:
        description_parts[-1] += f" via {presence.client}"
    description_parts.append(f"Objectives: {objectives}")
    description_parts.append(f"Traits: {traits_line}")
    if summary.get("summary"):
        description_parts.append(f"Summary: {summary['summary']}")
    if summary.get("currentFocus"):
        description_parts.append(f"Current focus: {summary['currentFocus']}")
    if summary.get("nextSteps"):
        description_parts.append(f"Next steps: {summary['nextSteps']}")
    task_children: list[EnvironmentNode] = []
    for task in sorted(tasks, key=lambda item: str(item.get("createdAt", "")), reverse=True)[:max_tasks]:
        task_children.append(
            EnvironmentNode(
                id=f"task-{task.get('id')}",
                label=str(task.get("title", task.get("id", "task"))),
                description=f"Status: {task.get('status', 'open')} (owner: {task.get('owner', 'unknown')})",
                tags={
                    "status": str(task.get("status", "open")),
                    "assignees": ",".join(task.get("assignees", [])),
                },
                children=[],
            )
        )
    return EnvironmentNode(
        id=f"agent-{agent_id}",
        label=str(profile.get("name", agent_id)),
        description=" ".join(description_parts),
        tags={
            "agentId": agent_id,
            "role": role,
            "presence": presence.state,
            "balance": str(balance),
        },
        children=task_children,
    )


def _governance_environment_node(tasks: list[dict[str, object]], *, limit_per_status: int = 8) -> EnvironmentNode:
    buckets: dict[str, list[EnvironmentNode]] = {}
    grouped: dict[str, list[dict[str, object]]] = defaultdict(list)
    for task in tasks:
        grouped[str(task.get("status", "open"))].append(task)
    for status, items in grouped.items():
        items_sorted = sorted(items, key=lambda item: str(item.get("createdAt", "")), reverse=True)[:limit_per_status]
        status_children = [
            EnvironmentNode(
                id=f"governance-task-{task.get('id')}",
                label=str(task.get("title", task.get("id", "task"))),
                description=(task.get("description") or "")[:180],
                tags={
                    "status": status,
                    "owner": str(task.get("owner", "")),
                },
                children=[],
            )
            for task in items_sorted
        ]
        buckets[status] = status_children
    status_nodes = [
        EnvironmentNode(
            id=f"governance-{status}",
            label=status.replace("_", " ").title(),
            description=f"{len(grouped.get(status, []))} tasks",
            tags={"status": status},
            children=buckets.get(status, []),
        )
        for status in sorted(grouped.keys())
    ]
    return EnvironmentNode(
        id="governance",
        label="Governance",
        description="Task backlog grouped by status",
        children=status_nodes,
    )


def _ledger_environment_node(accounts: dict[str, Account], ledger: LedgerSummary, *, limit: int = 12) -> EnvironmentNode:
    description = (
        f"Total credits: {ledger.totalCredits} • Issuance: {ledger.issuance} • Charges: {ledger.charges} • "
        f"Velocity: {ledger.velocity:.2f}/h"
    )
    account_nodes = [
        EnvironmentNode(
            id=f"account-{name}",
            label=name,
            description=f"Balance: {account.balance} cr",
            tags={"balance": str(account.balance)},
        )
        for name, account in sorted(accounts.items(), key=lambda item: item[1].balance, reverse=True)[:limit]
    ]
    return EnvironmentNode(
        id="ledger",
        label="Ledger",
        description=description,
        children=account_nodes,
    )


def _compose_environment_tree(
    *,
    config: dict[str, dict[str, object]],
    presence_store: dict[str, dict[str, object]],
    summaries: dict[str, dict[str, object]],
    tasks: list[dict[str, object]],
    accounts: dict[str, Account],
    ledger: LedgerSummary,
    max_tasks: int,
) -> EnvironmentNode:
    tasks_by_agent: dict[str, list[dict[str, object]]] = defaultdict(list)
    for task in tasks:
        for assignee in task.get("assignees", []):
            tasks_by_agent[str(assignee)].append(task)
    agent_nodes = []
    for agent_id, profile in sorted(config.items(), key=lambda item: item[1].get("name", item[0])):
        agent_nodes.append(
            _agent_environment_node(
                agent_id,
                profile,
                presence_store,
                summaries,
                tasks_by_agent.get(agent_id, []),
                accounts.get(agent_id),
                max_tasks=max_tasks,
            )
        )
    for agent_id, account in accounts.items():
        if agent_id in config:
            continue
        agent_nodes.append(
            _agent_environment_node(
                agent_id,
                {"id": agent_id, "name": agent_id.title(), "role": "external"},
                presence_store,
                summaries,
                tasks_by_agent.get(agent_id, []),
                account,
                max_tasks=max_tasks,
            )
        )
    agents_branch = EnvironmentNode(
        id="agents",
        label="Agents",
        description="Codex/Claude clients participating in the society",
        children=agent_nodes,
    )
    governance_branch = _governance_environment_node(tasks)
    ledger_branch = _ledger_environment_node(accounts, ledger)
    return EnvironmentNode(
        id="agent-society",
        label="Agent Society",
        description="Unified view of agents, governance tasks, and resource ledger",
        children=[agents_branch, governance_branch, ledger_branch],
    )


def _environment_context_lines(node: EnvironmentNode, depth: int = 0) -> list[str]:
    indent = "  " * depth
    line = f"{indent}- {node.label}: {node.description or ''}".rstrip()
    lines = [line]
    for child in node.children:
        lines.extend(_environment_context_lines(child, depth + 1))
    return lines


@app.get("/agents/presence", response_model=list[AgentPresenceView])
def agents_presence() -> list[AgentPresenceView]:
    config = _load_agent_config()
    presence_store = _load_presence()
    agent_ids = set(config.keys()) | set(presence_store.keys())
    records: list[AgentPresenceView] = []
    for agent_id in agent_ids:
        record = _ensure_presence(agent_id, presence_store)
        records.append(
            AgentPresenceView(
                agent_id=agent_id,
                presence=_presence_view(agent_id, record),
            )
        )
    _save_presence(presence_store)
    return sorted(records, key=lambda item: item.agent_id.lower())


@app.get("/timeline", response_model=list[TimelineEntry])
def timeline(limit: int = Query(50, ge=1, le=500)) -> list[TimelineEntry]:
    return _timeline_entries(limit)


@app.get("/timeline/stream")
async def timeline_stream(limit: int = Query(50, ge=1, le=500)) -> StreamingResponse:
    async def event_generator():
        seen = deque(maxlen=limit * 2)
        try:
            while True:
                entries = _timeline_entries(limit)
                for entry in entries:
                    key = f"{entry.timestamp.isoformat()}::{entry.agent}::{entry.prompt}::{entry.type}"
                    if key in seen:
                        continue
                    seen.append(key)
                    payload = json.dumps(entry.model_dump(mode="json"), ensure_ascii=False)
                    yield f"data: {payload}\n\n"
                await asyncio.sleep(1.0)
        except asyncio.CancelledError:
            return

    return StreamingResponse(event_generator(), media_type="text/event-stream")


@app.post("/agents/session", response_model=SessionView, status_code=201)
def create_session(payload: SessionCreate) -> SessionView:
    config = _load_agent_config()
    presence_store = _load_presence()
    agent_id = payload.agent_id
    if agent_id not in config and agent_id not in presence_store:
        raise HTTPException(status_code=404, detail=f"Agent '{agent_id}' is not registered")
    record = _ensure_presence(agent_id, presence_store)
    if record.get("sessionId") and record.get("state") == "online":
        raise HTTPException(status_code=409, detail="Agent already online")
    session_id = uuid.uuid4().hex
    now_iso = _iso_now()
    record.update(
        {
            "sessionId": session_id,
            "state": "online",
            "client": payload.client,
            "capabilities": list(payload.capabilities),
            "connectedAt": now_iso,
            "lastSeen": now_iso,
            "disconnectedAt": None,
        }
    )
    presence_store[agent_id] = record
    _save_presence(presence_store)
    return SessionView(session_id=session_id, agent_id=agent_id, presence=_presence_view(agent_id, record))


@app.post("/agents/session/{session_id}/heartbeat", response_model=SessionView)
def session_heartbeat(session_id: str, payload: SessionHeartbeat) -> SessionView:
    presence_store = _load_presence()
    agent_id, record = _find_agent_by_session(session_id, presence_store)
    record["state"] = payload.state
    if payload.capabilities is not None:
        record["capabilities"] = list(payload.capabilities)
    record["lastSeen"] = _iso_now()
    presence_store[agent_id] = record
    _save_presence(presence_store)
    return SessionView(session_id=session_id, agent_id=agent_id, presence=_presence_view(agent_id, record))


@app.delete("/agents/session/{session_id}", response_model=AgentPresenceView)
def close_session(session_id: str) -> AgentPresenceView:
    presence_store = _load_presence()
    agent_id, record = _find_agent_by_session(session_id, presence_store)
    now_iso = _iso_now()
    record.update(
        {
            "state": "offline",
            "sessionId": None,
            "lastSeen": now_iso,
            "disconnectedAt": now_iso,
        }
    )
    presence_store[agent_id] = record
    _save_presence(presence_store)
    return AgentPresenceView(agent_id=agent_id, presence=_presence_view(agent_id, record))


@app.post("/accounts", response_model=AccountView, status_code=201)
def create_account(payload: AccountCreate, db: Session = Depends(get_db)):
    account = Account(name=payload.name, balance=0)
    db.add(account)
    try:
        db.commit()
    except IntegrityError as exc:
        db.rollback()
        raise HTTPException(status_code=409, detail="Account already exists") from exc
    db.refresh(account)
    if payload.initial_balance:
        _record_entry(db, account, payload.initial_balance, "initial_balance", None)
        db.commit()
        db.refresh(account)
    return account


@app.get("/accounts/{name}", response_model=AccountView)
def fetch_account(name: str, db: Session = Depends(get_db)):
    account = _get_account(db, name)
    return account


@app.get("/accounts/{name}/entries", response_model=list[LedgerEntryView])
def list_entries(name: str, limit: int = 100, db: Session = Depends(get_db)):
    account = _get_account(db, name)
    entries: Iterable[LedgerEntry] = (
        db.query(LedgerEntry)
        .filter(LedgerEntry.account_id == account.id)
        .order_by(LedgerEntry.created_at.desc())
        .limit(limit)
        .all()
    )
    return list(entries)


@app.post("/accounts/{name}/issue", response_model=AccountView)
def issue_credits(name: str, payload: CreditOperation, db: Session = Depends(get_db)):
    account = _get_account(db, name)
    _record_entry(db, account, payload.amount, payload.reason, payload.correlation_id)
    db.commit()
    db.refresh(account)
    return account


@app.post("/accounts/{name}/charge", response_model=AccountView)
def charge_account(name: str, payload: CreditOperation, db: Session = Depends(get_db)):
    account = _get_account(db, name)
    if account.balance < payload.amount:
        raise HTTPException(status_code=402, detail="Insufficient credits")
    _record_entry(db, account, -payload.amount, payload.reason, payload.correlation_id)
    db.commit()
    db.refresh(account)
    return account


@app.post("/accounts/{name}/transfer", response_model=AccountView)
def transfer(name: str, payload: TransferOperation, db: Session = Depends(get_db)):
    source = _get_account(db, name)
    destination = _get_account(db, payload.destination)
    if source.balance < payload.amount:
        raise HTTPException(status_code=402, detail="Insufficient credits")
    _record_entry(db, source, -payload.amount, payload.reason, payload.correlation_id)
    _record_entry(db, destination, payload.amount, payload.reason, payload.correlation_id)
    db.commit()
    db.refresh(source)
    return source


@app.get("/agents/summary", response_model=list[AgentSummary])
def agents_summary(db: Session = Depends(get_db)) -> list[AgentSummary]:
    config = _load_agent_config()
    presence_store = _load_presence()
    accounts = {account.name: account for account in db.query(Account).all()}
    summaries: list[AgentSummary] = []

    def build_summary(agent_id: str, conf: dict[str, object], balance: int) -> AgentSummary:
        record = _ensure_presence(agent_id, presence_store)
        presence = _presence_view(agent_id, record)
        config_status = str(conf.get("status", "offline"))
        status = presence.state
        if config_status == "degraded":
            status = "degraded"
        elif config_status == "idle" and presence.state == "online":
            status = "online"
        elif config_status not in {"online", "offline", "away", "active", "idle", "degraded"}:
            status = presence.state
        return AgentSummary(
            id=agent_id,
            name=str(conf.get("name", agent_id.title())),
            role=str(conf.get("role", "agent")),
            status=status,
            balance=balance,
            objectives=list(conf.get("objectives", [])),
            traits=dict(conf.get("traits", {})),
            presence=presence,
        )

    for agent_id, conf in config.items():
        account = accounts.get(agent_id)
        balance = account.balance if account else 0
        summaries.append(build_summary(agent_id, conf, balance))
    for agent_id, account in accounts.items():
        if agent_id in config:
            continue
        conf: dict[str, object] = {}
        summaries.append(build_summary(agent_id, conf, account.balance))

    _save_presence(presence_store)
    return sorted(summaries, key=lambda item: item.name.lower())


@app.get("/telemetry/ledger-summary", response_model=LedgerSummary)
def ledger_summary(db: Session = Depends(get_db)) -> LedgerSummary:
    now = dt.datetime.utcnow().replace(tzinfo=dt.timezone.utc)
    total = db.query(func.coalesce(func.sum(Account.balance), 0)).scalar() or 0
    window_start = now - dt.timedelta(hours=24)
    entries: list[LedgerEntry] = (
        db.query(LedgerEntry)
        .filter(LedgerEntry.created_at >= window_start.replace(tzinfo=None))
        .all()
    )
    issuance = sum(entry.delta for entry in entries if entry.delta > 0)
    charges = sum(-entry.delta for entry in entries if entry.delta < 0)
    hours = max((now - window_start).total_seconds() / 3600, 1e-6)
    velocity = (issuance - charges) / hours
    return LedgerSummary(
        totalCredits=int(total),
        velocity=float(velocity),
        issuance=int(issuance),
        charges=int(charges),
        updatedAt=now,
    )


@app.get("/events/feed", response_model=list[EventRecord])
def events_feed(limit: int = Query(50, ge=1, le=200), owner: str | None = None) -> list[EventRecord]:
    if not EVENT_LOG_DIR.exists():
        return []
    events: list[EventRecord] = []
    owners = [owner] if owner else [p.name for p in EVENT_LOG_DIR.iterdir() if p.is_dir()]
    for space_owner in owners:
        raw_events = read_events(EVENT_LOG_DIR, space_owner)
        for event in raw_events:
            created_raw = event.created_at
            if isinstance(created_raw, str):
                normalized = created_raw.replace("Z", "+00:00")
                try:
                    created_dt = dt.datetime.fromisoformat(normalized)
                except ValueError:
                    created_dt = dt.datetime.utcnow().replace(tzinfo=dt.timezone.utc)
            elif isinstance(created_raw, dt.datetime):
                created_dt = created_raw if created_raw.tzinfo else created_raw.replace(tzinfo=dt.timezone.utc)
            else:
                created_dt = dt.datetime.utcnow().replace(tzinfo=dt.timezone.utc)
            events.append(
                EventRecord(
                    id=event.id,
                    type=event.type,
                    owner=event.owner,
                    actor=event.actor,
                    createdAt=created_dt,
                    content=event.content,
                    headers=event.headers,
                )
            )
    events.sort(key=lambda ev: ev.createdAt, reverse=True)
    return events[:limit]


@app.get("/agents/{agent_id}/memories", response_model=list[MemoryRecordView])
def agent_memories(agent_id: str, limit: int = Query(10, ge=1, le=50), query: str | None = None) -> list[MemoryRecordView]:
    events = read_events(EVENT_LOG_DIR, agent_id)
    if not events:
        return []
    records = events_to_records(events, query=query, top_k=limit)
    return [MemoryRecordView(**record.as_dict()) for record in records]


@app.get("/environment/tree", response_model=EnvironmentView)
def environment_tree(limit: int = Query(5, ge=1, le=20), db: Session = Depends(get_db)) -> EnvironmentView:
    config = _load_agent_config()
    presence_store = _load_presence()
    summaries = _load_persona_summaries()
    tasks = _load_tasks()
    accounts = {account.name: account for account in db.query(Account).all()}
    ledger_info = ledger_summary(db)
    tree = _compose_environment_tree(
        config=config,
        presence_store=presence_store,
        summaries=summaries,
        tasks=tasks,
        accounts=accounts,
        ledger=ledger_info,
        max_tasks=limit,
    )
    context = "\n".join(_environment_context_lines(tree))
    return EnvironmentView(tree=tree, context=context)


@app.get("/governance/tasks", response_model=list[GovernanceTask])
def list_tasks() -> list[GovernanceTask]:
    tasks = _load_tasks()
    return [_parse_task(task) for task in tasks]


@app.post("/governance/tasks", response_model=GovernanceTask, status_code=201)
def create_task(payload: GovernanceTaskPayload) -> GovernanceTask:
    now = dt.datetime.utcnow().replace(tzinfo=dt.timezone.utc)
    task_record = {
        "id": uuid.uuid4().hex,
        "owner": payload.owner,
        "actor": payload.actor,
        "title": payload.title,
        "status": "open",
        "description": payload.description,
        "assignees": payload.assignees or [],
        "createdAt": now.isoformat(),
    }
    with _TASK_LOCK:
        tasks = _load_tasks()
        tasks.append(task_record)
        _save_tasks(tasks)
    return _parse_task(task_record)


@app.post("/governance/tasks/{task_id}/status", response_model=GovernanceTask)
def update_task_status(task_id: str, payload: StatusUpdate) -> GovernanceTask:
    with _TASK_LOCK:
        tasks = _load_tasks()
        for task in tasks:
            if task.get("id") == task_id:
                task["status"] = payload.status
                _save_tasks(tasks)
                return _parse_task(task)
    raise HTTPException(status_code=404, detail="Task not found")


@app.get("/health")
def health() -> dict[str, str]:
    return {"status": "ok"}


def _parse_task(task: dict[str, object]) -> GovernanceTask:
    created = task.get("createdAt")
    if isinstance(created, str):
        try:
            created_dt = dt.datetime.fromisoformat(created)
        except ValueError:
            created_dt = dt.datetime.utcnow().replace(tzinfo=dt.timezone.utc)
    elif isinstance(created, dt.datetime):
        created_dt = created
    else:
        created_dt = dt.datetime.utcnow().replace(tzinfo=dt.timezone.utc)
    if created_dt.tzinfo is None:
        created_dt = created_dt.replace(tzinfo=dt.timezone.utc)
    return GovernanceTask(
        id=str(task.get("id")),
        owner=str(task.get("owner", "")),
        actor=str(task.get("actor", "")),
        title=str(task.get("title", "")),
        status=str(task.get("status", "open")),
        description=task.get("description"),
        assignees=list(task.get("assignees", [])),
        createdAt=created_dt,
    )
